import { Vector2, Vector3, Plane, EventDispatcher, MOUSE, TOUCH } from 'three'
import { getCameraRaycast } from '../core/view'

const STATE = {
  NONE: -1,
  ROTATE: 0,
  DOLLY: 1,
  PAN: 2,
  TOUCH_ROTATE: 3,
  TOUCH_DOLLY_PAN: 4,
}

const CLICK_TYPE = {
  NONE: -1,
  CLICK: 0,
  RIGHT_CLICK: 1,
}

const autoRotateSpeed = 1.0;
const autoRotationAngle = (360 / 60 / 60) * autoRotateSpeed;
const PIXELS_PER_ROUND = 1800;
const SCALE_STEP = 1.05;
const TOUCH_SCALE_STEP = 1.03;

export default class GestureControl  extends EventDispatcher {
    constructor(map) {
      super();
      this.$map = map;
      this.camera = map._camera;
      this.wrapper = map.$mapWrapper;

      this.enabled = true;
      this.scrollWheelZoomEnabled = true;

      this.onClickListener = null;

      this.onHoverListener = null;

      // Mouse buttons
      this.mouseButtons = { LEFT: MOUSE.PAN, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.ROTATE };

      // Touch fingers
      this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };

      this._addEventListeners();
      this._initVars();

      this.viewToWorld = function() {
        const plane = new Plane(new Vector3(0, 0, 1), 0);

        return function(point) {
            let raycaster = getCameraRaycast(this.$map, point);
            let result = new Vector3();
            raycaster.ray.intersectPlane(plane, result);
            return result;
        }
      }();
    }

    destroy() {
      this._initListeners(true);
    }

    reset() {
      this._removeEventListeners();
    }

    pan(start, end) {
      let vector = this.viewToWorld(start).sub(this.viewToWorld(end));

      this.$map._translate_(vector);
    }

    rotateLeft(angle = autoRotationAngle) {
      this.$map.rotateTo({ angle: this.$map.rotateAngle + angle, animate: false });
    }

    rotateRight(angle = autoRotationAngle) {
      this.$map.rotateTo({ angle: this.$map.rotateAngle - angle, animate: false });
    }

    rotateUp(angle = autoRotationAngle) {
      this.$map.tiltTo({ angle: this.$map.tiltAngle - angle, animate: false });
    }

    rotateDown(angle = autoRotationAngle) {
      this.$map.tiltTo({ angle: this.$map.tiltAngle + angle, animate: false });
    }

    _initVars() {
      this.state = STATE.NONE;
      this.clickType = CLICK_TYPE.NONE;
      this.clickStart = new Vector2();
      this.clickEnd = new Vector2();
      this.clickDelta = new Vector2();

      this.rotateStart = new Vector2();
      this.rotateEnd = new Vector2();
      this.rotateDelta = new Vector2();
  
      this.panStart = new Vector2();
      this.panEnd = new Vector2();
      this.panDelta = new Vector2();
  
      this.dollyStart = new Vector2();
      this.dollyEnd = new Vector2();
      this.dollyDelta = new Vector2();

      this.enableZoom = true;
      this.zoomSpeed = 1.0;
      this.enableRotate = true;
      this.rotateSpeed = 1.0;
      this.enablePan = true;
      this.panSpeed = 1.0;
    }

    _addEventListeners() {
      this.wrapper.addEventListener('contextmenu', this);

      this.wrapper.addEventListener('pointerdown', this, { passive: true });
      window.addEventListener('pointermove', this, { passive: true });
      window.addEventListener('pointerup', this, { passive: true });

      this.wrapper.addEventListener('touchstart', this, { passive: false });
      this.wrapper.addEventListener('touchmove', this, { passive: false });
      this.wrapper.addEventListener('touchend', this, { passive: true });

      this.wrapper.addEventListener('wheel', this, { passive: false });
    }

    _removeEventListeners() {
      this.wrapper.removeEventListener('contextmenu', this);

      this.wrapper.removeEventListener('pointerdown', this);
      window.removeEventListener('pointermove', this);
      window.removeEventListener('pointerup', this);

      this.wrapper.removeEventListener('touchstart', this);
      this.wrapper.removeEventListener('touchmove', this);
      this.wrapper.removeEventListener('touchend', this);

      this.wrapper.removeEventListener('wheel', this);
    }

    handleTouchStartRotate(event) {
			if (event.touches.length == 1) {
				this.rotateStart.set(event.touches[0].pageX, event.touches[0].pageY);
			} else {
				const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
				const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
				this.rotateStart.set(x, y);
			}
		}

		handleTouchStartPan(event) {
			if (event.touches.length == 1) {
				this.panStart.set(event.touches[0].pageX, event.touches[0].pageY);
			} else {
				const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
				const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
				this.panStart.set(x, y);
			}
		}

		handleTouchStartDolly(event) {
			const dx = event.touches[0].pageX - event.touches[1].pageX;
			const dy = event.touches[0].pageY - event.touches[1].pageY;
			const distance = Math.sqrt(dx * dx + dy * dy);
			this.dollyStart.set(0, distance);
		}

		handleTouchStartDollyPan( event ) {
			if (this.enableZoom) this.handleTouchStartDolly(event);
			if (this.enablePan) this.handleTouchStartPan(event);
		}

		handleTouchStartDollyRotate(event) {
			if (this.enableZoom) this.handleTouchStartDolly(event);
			if (this.enableRotate) this.handleTouchStartRotate(event);
		}

		handleTouchMoveRotate(event) {
			if (event.touches.length == 1) {
				this.rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
			} else {
				const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
				const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
				this.rotateEnd.set(x, y);
			}

			this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart).multiplyScalar(this.rotateSpeed);
			// const element = this.domElement;
			// rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height
			// rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight);
      this.rotateLeft((360 * this.rotateDelta.x) / PIXELS_PER_ROUND);
      this.rotateUp((360 * this.rotateDelta.y) / PIXELS_PER_ROUND);
			this.rotateStart.copy(this.rotateEnd);
		}

		handleTouchMovePan(event) {
			if (event.touches.length == 1) {
				this.panEnd.set(event.touches[0].pageX, event.touches[0].pageY);
			} else {
				const x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
				const y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
				this.panEnd.set(x, y);
			}
      
			//this.panDelta.subVectors(this.panEnd, this.panStart).multiplyScalar(this.panSpeed);
			//pan(panDelta.x, panDelta.y );
      this.pan(this.panStart, this.panEnd);
			this.panStart.copy(this.panEnd);
		}

		handleTouchMoveDolly(event) {
			const dx = event.touches[0].pageX - event.touches[1].pageX;
			const dy = event.touches[0].pageY - event.touches[1].pageY;
			const distance = Math.sqrt(dx * dx + dy * dy);
			this.dollyEnd.set(0, distance);
			this.dollyDelta.set(0, Math.pow(this.dollyEnd.y / this.dollyStart.y, this.zoomSpeed));
      // TODO
			let scale = Math.pow(SCALE_STEP, this.dollyDelta.y / 50);
      this.$map._scale_(scale);
			this.dollyStart.copy(this.dollyEnd);
		}

		handleTouchMoveDollyPan(event) {
			if (this.enableZoom) this.handleTouchMoveDolly(event);
			if (this.enablePan) this.handleTouchMovePan(event);
		}

		handleTouchMoveDollyRotate(event) {
			if (this.enableZoom) this.handleTouchMoveDolly(event);
			if (this.enableRotate) this.handleTouchMoveRotate(event);
		}

		handleTouchEnd(event) {
			if ((this.clickType !== CLICK_TYPE.NONE) && this.onClickListener) {
        const rect = this.wrapper.getBoundingClientRect();
        const x = this.clickStart.x - rect.left;
        const y = this.clickStart.y - rect.top;
        this.onClickListener({clickType: this.clickType, x: x, y: y, rawEvent: event});
      }
		}

    _onTouchStart(event) {
      if (this.enabled === false) return;

			event.preventDefault(); // prevent scrolling

      this.clickType = CLICK_TYPE.NONE;

			switch (event.touches.length) {
				case 1:
        {
          this.clickType = CLICK_TYPE.CLICK;
          this.clickStart.set(event.touches[0].clientX, event.touches[0].clientY);

					switch ( this.touches.ONE ) {
						case TOUCH.ROTATE:
							if (this.enableRotate === false) return;
							this.handleTouchStartRotate( event );
							this.state = STATE.TOUCH_ROTATE;
							break;

						case TOUCH.PAN:
							if (this.enablePan === false) return;
							this.handleTouchStartPan( event );
							this.state = STATE.TOUCH_PAN;
							break;

						default:
							this.state = STATE.NONE;
					}
					break;
        }

				case 2:
        {
					switch ( this.touches.TWO ) {
						case TOUCH.DOLLY_PAN:
							if (this.enableZoom === false && this.enablePan === false) return;
							this.handleTouchStartDollyPan( event );
							this.state = STATE.TOUCH_DOLLY_PAN;
							break;

						case TOUCH.DOLLY_ROTATE:
							if (this.enableZoom === false && this.enableRotate === false) return;
							this.handleTouchStartDollyRotate(event);
							this.state = STATE.TOUCH_DOLLY_ROTATE;
							break;

						default:
							this.state = STATE.NONE;
					}
					break;
        }

				default:
					this.state = STATE.NONE;
			}
    }
    _onTouchMove(event) {
      if (this.enabled === false) return;

			event.preventDefault(); // prevent scrolling

      this.clickType = CLICK_TYPE.NONE;

			switch (this.state) {
				case STATE.TOUCH_ROTATE:
					if (this.enableRotate === false) return;
					this.handleTouchMoveRotate(event);
					break;

				case STATE.TOUCH_PAN:
					if (this.enablePan === false) return;
					this.handleTouchMovePan(event);
					break;

				case STATE.TOUCH_DOLLY_PAN:
					if (this.enableZoom === false && this.enablePan === false) return;
					this.handleTouchMoveDollyPan(event);
					break;

				case STATE.TOUCH_DOLLY_ROTATE:
					if (this.enableZoom === false && this.enableRotate === false) return;
					this.handleTouchMoveDollyRotate(event);
					break;

				default:
					this.state = STATE.NONE;
			}
    }

    _onTouchEnd(event) {
      if ( this.enabled === false ) return;
			this.handleTouchEnd(event);
			this.state = STATE.NONE;
    }

    handleMouseDownRotate(event) {
			this.rotateStart.set(event.clientX, event.clientY);
		}

		handleMouseDownDolly(event) {
			this.dollyStart.set(event.clientX, event.clientY);
		}

		handleMouseDownPan(event) {
			this.panStart.set(event.clientX, event.clientY);
		}

		handleMouseMoveRotate(event) {
			this.rotateEnd.set(event.clientX, event.clientY);
			this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart).multiplyScalar(this.rotateSpeed);
			// const element = this.domElement;
			// rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
			// rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
      this.rotateLeft((360 * this.rotateDelta.x) / PIXELS_PER_ROUND);
      this.rotateUp((360 * this.rotateDelta.y) / PIXELS_PER_ROUND);
			this.rotateStart.copy(this.rotateEnd);
		}

		handleMouseMoveDolly(event) {
			this.dollyEnd.set(event.clientX, event.clientY);
			this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart);
			// if ( dollyDelta.y > 0 ) {
			// 	dollyOut( getZoomScale() );
			// } else if ( dollyDelta.y < 0 ) {
			// 	dollyIn( getZoomScale() );
			// }
      if (this.dollyDelta.y > 0) {
        this.$map.zoomOut(1 / TOUCH_SCALE_STEP)
      } else {
        this.$map.zoomIn(TOUCH_SCALE_STEP)
      }
			this.dollyStart.copy(this.dollyEnd);
		}

		handleMouseMovePan(event) {
			this.panEnd.set(event.clientX, event.clientY);
			//this.panDelta.subVectors(this.panEnd, this.panStart).multiplyScalar(this.panSpeed);;
			// pan( panDelta.x, panDelta.y );
      this.pan(this.panStart, this.panEnd);
			this.panStart.copy(this.panEnd);
		}

		handleMouseUp(event) {
			if ((this.clickType !== CLICK_TYPE.NONE) && this.onClickListener) {
        const rect = this.wrapper.getBoundingClientRect();
        const x = this.clickStart.x - rect.left;
        const y = this.clickStart.y - rect.top;
        this.onClickListener({clickType: this.clickType, x: x, y: y, rawEvent: event});
      }
		}

    onMouseDown(event) {
      if (this.enabled === false) return;

			let mouseAction = -1;
      this.clickType = CLICK_TYPE.NONE;
			
      switch (event.button) {
				case 0:
					mouseAction = this.mouseButtons.LEFT;
          this.clickType = CLICK_TYPE.CLICK;
          this.clickStart.set(event.clientX, event.clientY);
					break;

				case 1:
					mouseAction = this.mouseButtons.MIDDLE;
					break;

				case 2:
					mouseAction = this.mouseButtons.RIGHT;
          this.clickType = CLICK_TYPE.RIGHT_CLICK;
          this.clickStart.set(event.clientX, event.clientY);
					break;

				default:
					mouseAction = -1;
			}

			switch (mouseAction) {
				case MOUSE.DOLLY:
					if (this.enableZoom === false) return;
					this.handleMouseDownDolly(event);
					this.state = STATE.DOLLY;
					break;

				case MOUSE.ROTATE:
					if (event.ctrlKey || event.metaKey || event.shiftKey) {
						if (this.enablePan === false) return;
						this.handleMouseDownPan(event);
						this.state = STATE.PAN;
					} else {
						if (this.enableRotate === false) return;
						this.handleMouseDownRotate(event);
						this.state = STATE.ROTATE;
					}
					break;

				case MOUSE.PAN:
					if (event.ctrlKey || event.metaKey || event.shiftKey) {
						if (this.enableRotate === false) return;
						this.handleMouseDownRotate(event);
						this.state = STATE.ROTATE;
					} else {
						if (this.enablePan === false) return;
						this.handleMouseDownPan(event);
						this.state = STATE.PAN;
					}
					break;

				default:
					this.state = STATE.NONE;
			}
		}

		onMouseMove(event) {
			if (this.enabled === false) return;

      if (this.clickType !== CLICK_TYPE.NONE) {
        this.clickEnd.set(event.clientX, event.clientY);
        this.clickDelta.subVectors(this.clickEnd, this.clickStart);
        if (this.clickDelta.length() > 2) {
          this.clickType = CLICK_TYPE.NONE;
        }
      }

			switch (this.state) {
				case STATE.ROTATE:
					if (this.enableRotate === false) return;
					this.handleMouseMoveRotate(event);
					break;

				case STATE.DOLLY:
					if (this.enableZoom === false) return;
					this.handleMouseMoveDolly(event);
					break;

				case STATE.PAN:
					if (this.enablePan === false) return;
					this.handleMouseMovePan(event);
					break;
			}

      if (this.onHoverListener && this.wrapper.contains(event.target)) {
        const rect = this.wrapper.getBoundingClientRect();
        const x = event.clientX - rect.left;
        const y = event.clientY - rect.top;
        this.onHoverListener({x: x, y: y, rawEvent: event});
      }
		}

		onMouseUp(event) {
			if (this.enabled === false) return;
			this.handleMouseUp(event);
			this.state = STATE.NONE;
		}

    _onPointerDown(event) {
      switch (event.pointerType) {
				case 'mouse':
				case 'pen':
					this.onMouseDown(event);
					break;

				// TODO touch
			}
    }

    _onPointerMove(event) {
      switch (event.pointerType) {
				case 'mouse':
				case 'pen':
					this.onMouseMove(event);
					break;

				// TODO touch
			}
    }

    _onPointerUp(event) {
      switch (event.pointerType) {
				case 'mouse':
				case 'pen':
					this.onMouseUp(event);
					break;

				// TODO touch
			}
    }

    handleMouseWheel(event) {
			let delta = -event.deltaY / 50;
      let scale = Math.pow(SCALE_STEP, delta)
      this.$map._scale_(scale)
		}

    _onWheel(event) {
      if (this.enabled === false || this.enableZoom === false || (this.state !== STATE.NONE && this.state !== STATE.ROTATE)) return;

			event.preventDefault();

			this.handleMouseWheel(event);
    }

    handleEvent(event) {
      switch (event.type) {
        case 'touchstart':
          this._onTouchStart(event);
          break;
        case 'touchmove':
          this._onTouchMove(event);
          break;
        case 'touchend':
          this._onTouchEnd(event);
          break;
        case 'pointerdown':
          this._onPointerDown(event);
          break;
        case 'pointermove':
          this._onPointerMove(event);
          break;
        case 'pointerup':
          this._onPointerUp(event);
          break;
        case 'wheel':
          this._onWheel(event);
          break;
        case 'contextmenu':
          event.preventDefault();
          break;
      }
    }
}
